# -*- coding: utf-8 -*-
# @version        : 1.0
# @Creaet Time    : 2021/10/13 09:00
# @File           : case03.py
# @IDE            : PyCharm
# @desc           : 案例03，响应操作


"""
1. 声明响应模型
2. 额外响应模型：https://fastapi.tiangolo.com/zh/tutorial/extra-models/
3. JSON 兼容编码器：https://blog.csdn.net/xuqingskywolf/article/details/106383755
4. 直接返回响应
5. 返回状态码
6. 错误处理：https://fastapi.tiangolo.com/zh/tutorial/handling-errors/
"""
from datetime import datetime

"""
EmailStr字段需要单独下载：pip install pydantic[email]
"""

from fastapi import APIRouter, Response, status, HTTPException
from typing import Optional, Union, List, Dict
from pydantic import BaseModel, Field, EmailStr
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse, ORJSONResponse, HTMLResponse

case03 = APIRouter()


@case03.get(path="/",
            summary="案例3",
            description="初始接口",
            status_code=status.HTTP_200_OK,
            name="案例三",
            response_description="响应描述")
def case03_root():
    return "case03_root"


# =========================================================声明响应模型=================================================


class User(BaseModel):
    id: int
    username: str = Field(..., title="用户名")
    password: str = Field(..., title="密码")
    nickname: Optional[str] = Field(None, title="昵称")
    email: EmailStr = Field(None, title="邮箱")
    address: Optional[str] = Field(None, title="地址")
    telephone: str = Field(default=None, min_length=11, max_length=11, title="手机号")


@case03.post("/response/all", summary="声明响应模型", response_model=User)
def response_all(user: User):
    """
    response_model：表示声明响应的模型
    """
    return user


class UserOut(BaseModel):
    id: int
    username: str = Field(..., title="用户名")
    nickname: Optional[str] = Field(None, title="昵称")
    telephone: str = Field(default=None, min_length=11, max_length=11, title="手机号")


@case03.post("/response/model", summary="重新定义声明响应模型", response_model=UserOut)
def response_model(user: User):
    """
    只会返回 response_model 响应模型中定义的数据字段
    """
    return user


@case03.post("/response/exclude/unset", summary="响应模型，只返回用户传递的数据字段", response_model=User, response_model_exclude_unset=True)
def response_exclude_unset(user: User):
    """
    response_model_exclude_unset=True，表示在响应模型中只返回用户传递的数据字段
    """
    return user


@case03.post("/response/model/include", summary="响应模型，指定返回数据字段", response_model=User, response_model_include=["id", "username"])
def response_model_include(user: User):
    """
    response_model_include：用于指定响应模型中返回的数据字段
    """
    return user


@case03.post("/response/model/exclude", summary="响应模型，指定不返回数据字段，排除字段", response_model=User, response_model_exclude=["password", "telephone"])
def response_model_exclude(user: User):
    """
    response_model_exclude：用于指定响应模型不返回数据字段，排除字段
    """
    return user


# =========================================================额外响应模型=================================================


class UnionItem(BaseModel):
    description: str
    type: str


class AbstractOut(UnionItem):
    type = "car"


class DetailOut(UnionItem):
    type = "plane"
    size: int


union_items = {
    "item1": {"description": "All my friends drive a low rider", "type": "car"},
    "item2": {
        "description": "Music is my aeroplane, it's my aeroplane",
        "type": "plane",
        "size": 5,
    },
}


@case03.post("/response/model/union/{item}", summary="Union响应模型", response_model=Union[DetailOut, AbstractOut])
def response_model_union(item: str):
    """
    你可以将一个响应，声明为两种类型的 Union，这意味着该响应将是两种类型中的任何一种。

    定义一个 Union 类型时，首先包括最详细的类型，然后是不太详细的类型。
    在下面的示例中，更详细的 DetailOut 位于 Union[DetailOut，AbstractOut] 中的 AbstractOut 之前。
    """
    return union_items[item]


class Item(BaseModel):
    name: str
    description: str


items = [
    {"name": "Foo", "description": "There comes my hero"},
    {"name": "Red", "description": "It's my aeroplane"},
]


@case03.post("/response/model/list", summary="响应模型列表", response_model=List[Item])
def response_model_list():
    """
    声明返回一个响应模型列表
    """
    return items


@case03.post("/response/model/dict", summary=" dict 构成的响应", response_model=Dict[str, float])
def response_model_dict():
    """
    还可以使用一个任意的普通 dict 声明响应，仅声明键和值的类型，而不使用 Pydantic 模型。
    如果你事先不知道有效的字段/属性名称（对于 Pydantic 模型是必需的），这将很有用。
    在这种情况下，你可以使用 typing.Dict：
    """
    return {"foo": 2.3, "bar": 3.4}


# =========================================================JSON 兼容编码器==============================================


json_db = {}


class JsonItem(BaseModel):
    title: str
    timestamp: datetime
    description: Optional[str] = None


@case03.post("/json/item/{id}")
def create_json_item(id: str, item: JsonItem):
    """
    在某些情况下，您可能需要将数据类型（例如 Pydantic 模型）转换为与 JSON 兼容的数据（例如 dict、list 等）。

    假设您有一个仅接收 JSON 兼容数据的数据库 json_db。

    这个数据库不直接接收 datetime 对象，和 Pydantic 模型（具有属性的对象）。

    因此，需要将 datetime 对象转换为包含 ISO格式 数据的 str。Pydantic 模型转换为 dict

    在这个例子中，jsonable_encoder 将 Pydantic 模型转换为 dict，将 datetime 转换为 str。其值和子值都与 JSON 兼容。

    不使用 jsonable_encoder 的结果：得到的是一个 Pydantic 模型<br>
    {'test': JsonItem(title='string', timestamp=datetime.datetime(2021, 10, 15, 0, 0), description='string')}

    使用 jsonable_encoder 的结果：得到的是一个 dict<br>
    {'test': {'title': 'string', 'timestamp': '2021-10-15T00:00:00', 'description': 'string'}}

    如果没有return 则 response.body 默认为null
    """
    json_compatible_item_data = jsonable_encoder(item)
    json_db[id] = json_compatible_item_data
    print(json_db)


# =========================================================直接返回响应=================================================


@case03.post("/json/response", summary="直接返回响应，JSONResponse")
def json_response(item: JsonItem):
    """
    当你创建一个 FastAPI 路径操作 时，你可以正常返回以下任意一种数据：dict，list，Pydantic 模型，数据库模型等等。

    FastAPI 默认会使用 jsonable_encoder 将这些类型的返回值转换成 JSON 格式，

    然后，FastAPI 会在后台将这些兼容 JSON 的数据（比如字典）放到一个 JSONResponse 中，该 JSONResponse 会用来发送响应给客户端。

    但是你可以在你的 路径操作 中直接返回一个 JSONResponse。

    直接返回响应可能会有用处，比如返回自定义的响应头和 cookies。

    你可以返回任意 Response 或者任意 Response 的子类。，JSONResponse 本身是一个 Response 的子类。

    当你返回一个 Response 时，FastAPI 会直接传递它，不会做任何转换。
    """
    json_compatible_item_data = jsonable_encoder(item)
    return JSONResponse(content=json_compatible_item_data)


@case03.get("/custom/response", summary="直接返回响应，自定义 Response")
def custom_response():
    """
    上面的例子说明了FastAPI的默认响应方式，以及 JSONResponse 例子

    现在，让我们看看你如何才能返回一个自定义的响应。

    假设你想要返回一个 XML 响应。

    你可以把你的 XML 内容放到一个字符串中，放到一个 Response 中，然后返回。

    其他全部的响应都继承自主类 Response。

    Response 类接受如下参数：

    - content - 一个 str 或者 bytes。
    - status_code - 一个 int 类型的 HTTP 状态码。
    - headers - 一个由字符串组成的 dict。
    - media_type - 一个给出媒体类型的 str，比如 "text/html"。
    """
    data = """<?xml version="1.0"?>
        <shampoo>
        <Header>
            Apply shampoo here.
        </Header>
        <Body>
            You'll have to use soap here.
        </Body>
        </shampoo>
        """
    return Response(content=data, media_type="application/xml")


@case03.get("/orjson/response", summary="声明响应，ORJSONResponse", response_class=ORJSONResponse)
def orjson_response():
    """
    如果你需要压榨性能，你可以安装并使用 orjson 并将响应设置为 ORJSONResponse。

    导入你想要使用的 Response 类（子类）然后在 路径操作装饰器 中声明它。

    安装：pip install orjson
    """
    return [{"item_id": "Foo"}]


@case03.get("/html/response", response_class=HTMLResponse, summary="声明响应，HTMLResponse")
async def html_response():
    return """
    <html>
        <head>
            <title>Some HTML in here</title>
        </head>
        <body>
            <h1>Look ma! HTML!</h1>
        </body>
    </html>
    """


@case03.get("/custom/html/response", response_class=HTMLResponse, summary="自定义响应后，声明响应，HTMLResponse")
async def custom_html_response():
    """
    路径操作函数 直接返回的 Response 不会被接口文档记录（比如，Content-Type 不会被文档记录），并且在接口文档中也是不可见的。而是显示默认的 JSONResponse

    如果你想要在函数内重载响应，但是同时想在接口文档中显示所使用的 Media type，你可以使用 response_class 参数并返回一个 Response 对象。

    接着 response_class 参数只会被用来在接口文档中生成 Media type，你的 Response 用来返回响应。
    """
    html_content = """
        <html>
            <head>
                <title>Some HTML in here</title>
            </head>
            <body>
                <h1>Look ma! HTML!</h1>
            </body>
        </html>
        """
    return HTMLResponse(content=html_content, status_code=200)


"""
HTMLResponse
如上文所述，接受文本或字节并返回 HTML 响应。

PlainTextResponse
接受文本或字节并返回纯文本响应。

JSONResponse
接受数据并返回一个 application/json 编码的响应。
如上文所述，这是 FastAPI 中使用的默认响应。

ORJSONResponse
如上文所述，ORJSONResponse 是一个使用 orjson 的快速的可选 JSON 响应。
ORJSONResponse 可能是一个更快的选择。

UJSONResponse
UJSONResponse 是一个使用 ujson 的可选 JSON 响应。
在处理某些边缘情况时，ujson 不如 Python 的内置实现那么谨慎。

RedirectResponse
返回 HTTP 重定向。默认情况下使用 307 状态代码（临时重定向）。

StreamingResponse
采用异步生成器或普通生成器/迭代器，然后流式传输响应主体。
对类似文件的对象使用 StreamingResponse
如果您有类似文件的对象（例如，由 open() 返回的对象），则可以在 StreamingResponse 中将其返回。

FileResponse
异步传输文件作为响应。
与其他响应类型相比，接受不同的参数集进行实例化：
- path - 要流式传输的文件的文件路径。
- headers - 任何自定义响应头，传入字典类型。
- media_type - 给出媒体类型的字符串。如果未设置，则文件名或路径将用于推断媒体类型。
- filename - 如果给出，它将包含在响应的 Content-Disposition 中。
文件响应将包含适当的 Content-Length，Last-Modified 和 ETag 的响应头。
"""


# =========================================================状态码=================================================


@case03.post("/response/code", summary="指定默认响应状态码", status_code=200)
def response_code(user: User):
    """
    status_code：表示响应状态码
    """
    return user


@case03.post("/response/code/status", summary="指定默认响应状态码，使用预定义状态码", status_code=status.HTTP_200_OK)
def response_code_status(user: User):
    """
    status 中包含了很多已经定义好的状态码，会更加直观
    """
    return user


@case03.get("/response/code/condition/{id}", summary="自定义响应状态码")
def response_code_condition(id: int, response: Response):
    if id < 10:
        response.status_code = status.HTTP_404_NOT_FOUND

    return id


# =========================================================错误处理=================================================


@case03.get("/response/exception/{id}", summary="错误处理")
def response_exception(id: int):
    if id < 10:
        # 注意：这里使用的是raise抛出异常，而不是return
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="未找到此ID")

    return id